#! /bin/sh
#
# Laptop mode tools module: core laptop mode functionality
#

# Remove an option (the first parameter) of the form option=<number> from
# a mount options string (the rest of the parameters).
remove_numeric_mount_option () {
	OPT="$1"
	shift
	echo ",$*," | sed		\
	 -e 's|,'"$OPT"'=[0-9]*,|,|g'	\
	 -e 's/,,*/,/g'			\
	 -e 's/^,//'			\
	 -e 's/,$//'
}

# Remove an option (the first parameter) without any arguments from
# a mount option string (the rest of the parameters).
remove_fixed_mount_option () {
	OPT="$1"
	shift
	echo ",$*," | sed		\
	 -e 's|,'"$OPT"',|,|g'		\
	 -e 's/,,*/,/g'			\
	 -e 's/^,//'			\
	 -e 's/,$//'
}

# Find out the state of an atime option ("atime"/"noatime"/"relatime"/"norelatime")
# in a set of mount options, and use this state to replace the value of the
# option in another mount options string.
#
# Example:
# replace_atime_mount_option defaults,user=1000,atime defaults,noatime
#
# This yields "defaults,atime".
replace_atime_mount_option () {
	REPLACEMENT_OPTS="$1"
	OPTS="$2"
	PARSEDOPTS="$(remove_fixed_mount_option atime $OPTS)"
	PARSEDOPTS="$(remove_fixed_mount_option noatime $PARSEDOPTS)"
	PARSEDOPTS="$(remove_fixed_mount_option relatime $PARSEDOPTS)"	
	PARSEDOPTS="$(remove_fixed_mount_option norelatime $PARSEDOPTS)"	

	case ",$REPLACEMENT_OPTS," in
	*",relatime,"*)
		echo "$PARSEDOPTS,relatime"
		;;
	*",noatime,"*)
		echo "$PARSEDOPTS,noatime"
		;;
	*)
		# Kind of strange: to go from relatime to atime, we have to
		# explicitly specify norelatime.
	 	echo "$PARSEDOPTS,atime,norelatime"
	 	;;
	esac
}

# Find out the state of a numbered option (e.g. "commit=NNN") in
# a set of options, and use this state to replace the
# value of the option in another mount options string. 
#
# Example:
# replace_numeric_mount_option commit defaults,user=1000,commit=3 defaults,commit=7
#
# This example yields "defaults,commit=3".
replace_numeric_mount_option () {
	OPT="$1"
	DEF_OPT="$2"
	REPLACEMENT_OPTS="$3"
	OPTS="$4"	
	PARSEDOPTS="$(remove_numeric_mount_option $OPT $OPTS)"
	
	if echo ",$REPLACEMENT_OPTS," | grep ",$OPT=[0123456789]+," > /dev/null ; then
		echo -n "$PARSEDOPTS,$OPT="
		echo ",$REPLACEMENT_OPTS," | sed \
		 -e 's/.*,'"$OPT"'=//'	\
		 -e 's/,.*//'
	else
		# Option not present in REPLACEMENT_OPTS: use the default.
		echo "$PARSEDOPTS,$DEF_OPT"
	fi
}

deduce_fstype () {
	MP="$1"
	# My root filesystem unfortunately has type "unknown" in
	# /etc/mtab. If we encounter "unknown", we try to get the
	# type from fstab. This still might be wrong, in which
	# case the code further down will issue a big warning.
	sed 's/[[:space:]]*#.*$//' /etc/fstab |
	while read FSTAB_DEV FSTAB_MP FSTAB_FST FSTAB_OPTS FSTAB_DUMP FSTAB_DUMP ; do
		if [ "$FSTAB_MP" = "$MP" ]; then
			echo "$FSTAB_FST"
			exit 0
		fi
	done
}


#
# Set kernel setting, showing an error if this fails.
#
# Parameter 1: sysctl/proc path
# Parameter 2: the value
#
set_sysctl() {
	log "VERBOSE" "Executing: echo $2 > $1"
	if ! echo "$2" > "$1" ; then
		log "MSG" "SETTING OF KERNEL PARAMETER FAILED: echo $2 \> $1"
	fi
}


if [ "$CONTROL_READAHEAD" -ne 0 ] ; then
	if /sbin/blockdev --help 2>&1 | grep -Fq -- '--setfra' ; then
		READAHEAD_OPTION=--setfra					
	else
		READAHEAD_OPTION=--setra
		if [ "$KLEVEL" = "2.4" ] ; then
			log "MSG" "Warning: Running a 2.4 kernel with blockdev that does not support --setfra."
			log "MSG" "File system readahead will not function properly."
		fi
	fi
fi



if [ $CONTROL_NOATIME -eq 1 ] ; then
	if [ "$KLEVEL" = "2.4" ] ; then
		log "VERBOSE" "Relatime is not supported on 2.4 kernels. Using noatime instead" 
		USE_RELATIME=0
	elif [ "$KLEVEL" = "2.6" -a "$KMINOR" -lt 23 ] ; then
		log "VERBOSE" "Relatime is not supported on kernels before 2.6.23. Using noatime instead."
		USE_RELATIME=0
	fi
	if [ "$USE_RELATIME" = 1 ] ; then
		NOATIME_OPT=",relatime"
	else
		NOATIME_OPT=",noatime"
	fi
fi


# Adjust kernel settings and mount options (but only if data loss 
# sensitive features are active)
if [ "$ACTIVATE_WITH_POSSIBLE_DATA_LOSS" -eq 1 ] ; then
	# Take MAX_LOST_WORK_SECONDS from LM_BATT_MAX_LOST_WORK_SECONDS or LM_AC_MAX_LOST_WORK_SECONDS_WITH_LM, depending on power state.
	MAX_LOST_WORK_SECONDS=$LM_BATT_MAX_LOST_WORK_SECONDS
	if [ $ON_AC -eq 1 ] ; then
		MAX_LOST_WORK_SECONDS=$LM_AC_MAX_LOST_WORK_SECONDS
	fi

	AGE=$((100*$MAX_LOST_WORK_SECONDS))
	XFS_AGE=$(($XFS_HZ*$MAX_LOST_WORK_SECONDS))

	if [ -d /proc/sys/vm/pagebuf ] ; then
		# (For 2.4 and early 2.6.)
		# This only needs to be set, not reset -- it is only used when
		# laptop mode is enabled.
		log "VERBOSE" "Adjusting XFS kernel parameters for 2.4 and early 2.6 kernels."
		set_sysctl /proc/sys/vm/pagebuf/lm_flush_age  $XFS_AGE
		set_sysctl /proc/sys/fs/xfs/lm_sync_interval  $XFS_AGE
	elif [ -f /proc/sys/fs/xfs/lm_age_buffer ] ; then
		# (A couple of early 2.6 laptop mode patches had these.)
		# This only needs to be set, not reset -- it is only used when
		# laptop mode is enabled.
		log "VERBOSE" "Adjusting XFS kernel parameters for early patched 2.6 kernels."
		set_sysctl /proc/sys/fs/xfs/lm_age_buffer    $XFS_AGE
		set_sysctl /proc/sys/fs/xfs/lm_sync_interval $XFS_AGE
	elif [ -f /proc/sys/fs/xfs/age_buffer ] ; then
		# (2.6.6)
		# But not for these -- they are also used in normal
		# operation.
		log "VERBOSE" "Adjusting XFS kernel parameters for 2.6.6 kernel."
		set_sysctl /proc/sys/fs/xfs/age_buffer       $XFS_AGE 
		set_sysctl /proc/sys/fs/xfs/sync_interval    $XFS_AGE
	elif [ -f /proc/sys/fs/xfs/age_buffer_centisecs ] ; then
		# (2.6.7 upwards)
		# And not for these either. These are in centisecs,
		# not USER_HZ, so we have to use $AGE, not $XFS_AGE.
		log "VERBOSE" "Adjusting XFS kernel parameters for >2.6.7 kernel."
		set_sysctl /proc/sys/fs/xfs/age_buffer_centisecs  $AGE
		set_sysctl /proc/sys/fs/xfs/xfssyncd_centisecs    $AGE
		set_sysctl /proc/sys/fs/xfs/xfsbufd_centisecs     3000
	fi

	case "$KLEVEL" in
		"2.4")
			log "VERBOSE" "Adjusting 2.4 kernel parameters to enable laptop mode."
			set_sysctl /proc/sys/vm/laptop_mode   1
			set_sysctl /proc/sys/vm/bdflush       "30 500 0 0 $AGE $AGE 60 20 0"
			;;
		"2.6")
			log "VERBOSE" "Adjusting 2.6 kernel parameters to enable laptop mode."
			set_sysctl /proc/sys/vm/laptop_mode		  "$LM_SECONDS_BEFORE_SYNC"
			set_sysctl /proc/sys/vm/dirty_writeback_centisecs "$AGE"
			set_sysctl /proc/sys/vm/dirty_expire_centisecs    "$AGE"
			set_sysctl /proc/sys/vm/dirty_ratio		  "$LM_DIRTY_RATIO"
			set_sysctl /proc/sys/vm/dirty_background_ratio    "$LM_DIRTY_BACKGROUND_RATIO"
			;;
	esac
	if [ $CONTROL_MOUNT_OPTIONS -eq 1 ]; then
		log "VERBOSE" "Remounting filesystems."
		# The -r flag makes 'read' preserve backslashes read from
		# mtab. Spaces are represented by \040 in mtab. (also UTF-8)
		cat /etc/mtab | while read -r DEV MP FST OPTS DUMP PASS ; do
			# Now expand the escapes, inside quotes to preserve the spaces :)
			MP="$(echo $MP)"
			case "$FST" in 
				rootfs|unionfs|tmpfs|squashfs|sysfs|usbfs|proc|devpts)
				  	continue
					;;
			esac
					
			DO=0
			
			case " $PARTITIONS " in
				*" $DEV "*)
					DO=1
					log "VERBOSE" "$DEV found in PARTITIONS."
					;;
				*)
					log "VERBOSE" "$DEV not found in PARTITIONS."
					;;
			esac
			case " $PARTITIONS " in
				*" $MP "*)
					DO=1
					log "VERBOSE" "$MP found in PARTITIONS."
					;;
				*)
					log "VERBOSE" "$MP not found in PARTITIONS."
			esac
			case " $PARTITIONS " in
				*" auto "*)
					log "VERBOSE" "Checking $DEV against HD because PARTITIONS contains \"auto\"."
					for THISHD in $HD ; do
						log "VERBOSE" "   Considering $THISHD."
						case " $DEV" in *"$THISHD"*)
							DO=1
							log "VERBOSE" "   $DEV contains $THISHD, which is in HD, so we will remount it."
							break
							;;
						esac
					done
					;;
			esac
			if [ "$DO" -ne 0 ] ; then
				log "VERBOSE" "Original options: $OPTS"
				if [ "$WAS_ACTIVE" -eq 0 ] ; then
					# Coming from inactive state: save last known mount options for the device.
					log "VERBOSE" "Updating /var/run/laptop-mode-tools/nolm-mountopts." 
					if [ -f /var/run/laptop-mode-tools/nolm-mountopts ] ; then 
						sed -i "s|^$DEV .*$||" /var/run/laptop-mode-tools/nolm-mountopts
					fi
					echo $DEV $OPTS >> /var/run/laptop-mode-tools/nolm-mountopts
				else
					log "VERBOSE" "Not updating /var/run/laptop-mode-tools/nolm-mountopts because laptop mode was already active."
				fi
				if [ "$FST" = 'unknown' ]; then
					log "VERBOSE" "Deducing fstype for $MP."
					FST=$(deduce_fstype $MP)
					log "VERBOSE" "Deduced fstype for $MP as $FST."
				fi
				# Strip stuff like ext3,ext2 into just ext3.
				log "VERBOSE" "Reducing file system type." 
				FST=${FST%%,*}
				case "$FST" in
					"ext3"|"reiserfs"|"ext4dev"|"ext4")
						log "VERBOSE" "Removing commit mount option from original options." 
						PARSEDOPTS="$(remove_numeric_mount_option commit "$OPTS")"
						log "VERBOSE" "Executing: mount $DEV $MP -t $FST -o remount,$PARSEDOPTS,commit=$MAX_LOST_WORK_SECONDS$NOATIME_OPT"
						if (! mount $DEV "$MP" -t $FST -o remount,$PARSEDOPTS,commit=$MAX_LOST_WORK_SECONDS$NOATIME_OPT) ; then
							if [ "$FST" = "ext3" -a "$MP" = "/" ] ; then
								echo "BIG FAT WARNING: Your root filesystem mounted as ext3 seems to lack support for"
								echo "the commit mount option. This usually means that your root filesystem is"
								echo "mounted as ext2 because there is no ext3 support in the kernel at boot time,"
								echo "usually because you compiled ext3 as a module and don't load it in an initrd."
								echo "Note that on recent 2.6 kernels, /proc/mounts shows the correct fs type for"
								echo "the device /dev/root. You can check your actual root filesystem mount type"
								echo "there. To fix the problem, either make ext3 available at boot time by compiling"
								echo "it statically into the kernel, or configure the correct filesystem type in"
								echo "/etc/fstab."
							fi
						fi
						;;
					*)
						log "VERBOSE" "Executing: mount $DEV $MP -t $FST -o remount,$OPTS$NOATIME_OPT"
						mount $DEV "$MP" -t $FST -o remount,$OPTS$NOATIME_OPT
						;;
				esac
				if [ -b $DEV -a "$CONTROL_READAHEAD" -ne 0 ] ; then
					log "VERBOSE" "Executing: /sbin/blockdev $READAHEAD_OPTION $(($LM_READAHEAD * 2)) $DEV"
					log "VERBOSE" "`/sbin/blockdev $READAHEAD_OPTION $(($LM_READAHEAD * 2)) $DEV 2>&1`"
				fi
			fi
		done
	fi
else
	# DEACTIVATE w.r.t. kernel options and mount point settings
	U_AGE=$((100*$DEF_UPDATE))
	B_AGE=$((100*$DEF_MAX_AGE))
	set_sysctl /proc/sys/vm/laptop_mode 0
	if [ -f /proc/sys/fs/xfs/age_buffer -a ! -f /proc/sys/fs/xfs/lm_age_buffer ] ; then
		# These need to be restored, if there are no lm_*.
		log "VERBOSE" "Restoring default XFS settings (pre-centisecs version)."
		set_sysctl /proc/sys/fs/xfs/age_buffer    $(($XFS_HZ*$DEF_XFS_AGE_BUFFER))
		set_sysctl /proc/sys/fs/xfs/sync_interval $(($XFS_HZ*$DEF_XFS_SYNC_INTERVAL))
	elif [ -f /proc/sys/fs/xfs/age_buffer_centisecs ] ; then
		# These need to be restored as well.
		log "VERBOSE" "Restoring default XFS settings."
		set_sysctl /proc/sys/fs/xfs/age_buffer_centisecs  $((100*$DEF_XFS_AGE_BUFFER))
		set_sysctl /proc/sys/fs/xfs/xfssyncd_centisecs    $((100*$DEF_XFS_SYNC_INTERVAL))
		set_sysctl /proc/sys/fs/xfs/xfsbufd_centisecs     $((100*$DEF_XFS_BUFD_INTERVAL))
	fi
	case "$KLEVEL" in
		"2.4")
			log "VERBOSE" "Adjusting 2.4 kernel parameters to disable laptop mode."
			set_sysctl /proc/sys/vm/bdflush "30 500 0 0 $U_AGE $B_AGE 60 20 0"
			;;
		"2.6")
			log "VERBOSE" "Adjusting 2.6 kernel parameters to disable laptop mode."
			set_sysctl /proc/sys/vm/dirty_writeback_centisecs   "$U_AGE"
			set_sysctl /proc/sys/vm/dirty_expire_centisecs      "$B_AGE"
			set_sysctl /proc/sys/vm/dirty_ratio		    "$NOLM_DIRTY_RATIO"
			set_sysctl /proc/sys/vm/dirty_background_ratio	    "$NOLM_DIRTY_BACKGROUND_RATIO"
			;;
	esac
	if [ $CONTROL_MOUNT_OPTIONS -eq 1 ] ; then
		log "VERBOSE" "Remounting filesystems."
		# The -r flag makes 'read' preserve backslashes read from
		# mtab. Spaces are represented by \040 in mtab. (also UTF-8)
		cat /etc/mtab | while read -r DEV MP FST OPTS DUMP PASS ; do
			# Now expand the escapes, inside quotes to preserve the spaces :)
			MP="$(echo $MP)"
			DO=0
			case " $PARTITIONS " in
				*" $DEV "*)
					DO=1
					log "VERBOSE" "$DEV found in PARTITIONS."
					;;
				*)
					log "VERBOSE" "$DEV not found in PARTITIONS."
					;;
			esac
			case " $PARTITIONS " in
				*" $MP "*)
					DO=1
					log "VERBOSE" "$MP found in PARTITIONS."
					;;
				*)
					log "VERBOSE" "$MP not found in PARTITIONS."
					;;
			esac
			case " $PARTITIONS " in
				*" auto "*)
					log "VERBOSE" "Checking $DEV against HD because PARTITIONS contains \"auto\"."
					for THISHD in $HD ; do
						log "VERBOSE" "   Considering $THISHD."
						case " $DEV" in *"$THISHD"*)
							DO=1
							log "VERBOSE" "   $DEV contains $THISHD, which is in HD, so we will remount it."
							break
							;;
						esac
					done
					;;
			esac
			if [ "$DO" -ne 0 ] ; then
				# Reset commit and atime options to defaults.
				log "VERBOSE" "Original options: $OPTS"
				if [ "$FST" = 'unknown' ]; then
					log "VERBOSE" "Deducing fstype for $MP."
					FST=$(deduce_fstype $MP)
					log "VERBOSE" "Deduced fstype for $MP as $FST."
				fi
				
				# Strip stuff like ext3,ext2 into just ext3.
				log "VERBOSE" "Reducing file system type."
				FST=${FST%%,*}
				
				# Retrieve original non-laptop mode mount options and restore them.
				# If the file that stores them doesn't exist, then laptop mode
				# has never been started.
				if [ "$WAS_ACTIVE" -ne 0 -a -f /var/run/laptop-mode-tools/nolm-mountopts ] ; then						
					SAVED_OPTS=`grep "^$DEV " /var/run/laptop-mode-tools/nolm-mountopts`
					SAVED_OPTS=${SAVED_OPTS#* } # trim device name
				
					case "$FST" in
						"ext3"|"reiserfs"|"ext4dev"|"ext4")								
							PARSEDOPTS="$(replace_numeric_mount_option commit commit=0 $SAVED_OPTS $OPTS)"
							PARSEDOPTS="$(replace_atime_mount_option $SAVED_OPTS $PARSEDOPTS)"
							log "VERBOSE" "Executing: mount $DEV $MP -t $FST -o remount,$PARSEDOPTS"
							mount $DEV "$MP" -t $FST -o remount,$PARSEDOPTS
							;;
						*)
							PARSEDOPTS="$(replace_atime_mount_option $SAVED_OPTS $OPTS)"
							log "VERBOSE" "Executing: mount $DEV $MP -t $FST -o remount,$PARSEDOPTS"
							mount $DEV "$MP" -t $FST -o remount,$PARSEDOPTS
							;;
					esac
				else
					log "VERBOSE" "No saved mount options, so apparently we never remounted this filesystem during this session."
					log "VERBOSE" "Not remounting."
				fi
				if [ -b $DEV -a "$CONTROL_READAHEAD" -ne 0 ] ; then
					log "VERBOSE" "Executing: /sbin/blockdev $READAHEAD_OPTION $(($NOLM_READAHEAD * 2)) $DEV"
					log "VERBOSE" "`/sbin/blockdev $READAHEAD_OPTION $(($NOLM_READAHEAD * 2)) $DEV 2>&1`"
				fi
			fi
		done
	fi
fi

